package modulo4.model;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;

public class ValidadorHTML {

    private static final Set<String> etiquetasConEtiquetaFinalRequerida;
    private static final Set<String> etiquetasConEtiquetaFinalOpcional;
    private static final Set<String> etiquetasConEtiquetaFinalVacia;

    static {
        etiquetasConEtiquetaFinalRequerida = new HashSet<>();
        etiquetasConEtiquetaFinalRequerida.add("a");
        etiquetasConEtiquetaFinalRequerida.add("abbr");
        etiquetasConEtiquetaFinalRequerida.add("address");
        etiquetasConEtiquetaFinalRequerida.add("article");
        etiquetasConEtiquetaFinalRequerida.add("aside");
        etiquetasConEtiquetaFinalRequerida.add("audio");
        etiquetasConEtiquetaFinalRequerida.add("b");
        etiquetasConEtiquetaFinalRequerida.add("bb");
        etiquetasConEtiquetaFinalRequerida.add("bdo");
        etiquetasConEtiquetaFinalRequerida.add("blockquote");
        etiquetasConEtiquetaFinalRequerida.add("button");
        etiquetasConEtiquetaFinalRequerida.add("canvas");
        etiquetasConEtiquetaFinalRequerida.add("caption");
        etiquetasConEtiquetaFinalRequerida.add("cite");
        etiquetasConEtiquetaFinalRequerida.add("code");
        etiquetasConEtiquetaFinalRequerida.add("datagrid");
        etiquetasConEtiquetaFinalRequerida.add("datalist");
        etiquetasConEtiquetaFinalRequerida.add("del");
        etiquetasConEtiquetaFinalRequerida.add("details");
        etiquetasConEtiquetaFinalRequerida.add("dfn");
        etiquetasConEtiquetaFinalRequerida.add("dialog");
        etiquetasConEtiquetaFinalRequerida.add("div");
        etiquetasConEtiquetaFinalRequerida.add("dl");
        etiquetasConEtiquetaFinalRequerida.add("em");
        etiquetasConEtiquetaFinalRequerida.add("fieldset");
        etiquetasConEtiquetaFinalRequerida.add("figure");
        etiquetasConEtiquetaFinalRequerida.add("footer");
        etiquetasConEtiquetaFinalRequerida.add("form");
        etiquetasConEtiquetaFinalRequerida.add("h1");
        etiquetasConEtiquetaFinalRequerida.add("h2");
        etiquetasConEtiquetaFinalRequerida.add("h3");
        etiquetasConEtiquetaFinalRequerida.add("h4");
        etiquetasConEtiquetaFinalRequerida.add("h5");
        etiquetasConEtiquetaFinalRequerida.add("h6");
        etiquetasConEtiquetaFinalRequerida.add("header");
        etiquetasConEtiquetaFinalRequerida.add("i");
        etiquetasConEtiquetaFinalRequerida.add("iframe");
        etiquetasConEtiquetaFinalRequerida.add("ins");
        etiquetasConEtiquetaFinalRequerida.add("kbd");
        etiquetasConEtiquetaFinalRequerida.add("label");
        etiquetasConEtiquetaFinalRequerida.add("legend");
        etiquetasConEtiquetaFinalRequerida.add("map");
        etiquetasConEtiquetaFinalRequerida.add("mark");
        etiquetasConEtiquetaFinalRequerida.add("menu");
        etiquetasConEtiquetaFinalRequerida.add("meter");
        etiquetasConEtiquetaFinalRequerida.add("nav");
        etiquetasConEtiquetaFinalRequerida.add("noscript");
        etiquetasConEtiquetaFinalRequerida.add("object");
        etiquetasConEtiquetaFinalRequerida.add("ol");
        etiquetasConEtiquetaFinalRequerida.add("output");
        etiquetasConEtiquetaFinalRequerida.add("pre");
        etiquetasConEtiquetaFinalRequerida.add("progress");
        etiquetasConEtiquetaFinalRequerida.add("q");
        etiquetasConEtiquetaFinalRequerida.add("rp");
        etiquetasConEtiquetaFinalRequerida.add("rt");
        etiquetasConEtiquetaFinalRequerida.add("ruby");
        etiquetasConEtiquetaFinalRequerida.add("samp");
        etiquetasConEtiquetaFinalRequerida.add("script");
        etiquetasConEtiquetaFinalRequerida.add("section");
        etiquetasConEtiquetaFinalRequerida.add("select");
        etiquetasConEtiquetaFinalRequerida.add("small");
        etiquetasConEtiquetaFinalRequerida.add("span");
        etiquetasConEtiquetaFinalRequerida.add("strong");
        etiquetasConEtiquetaFinalRequerida.add("style");
        etiquetasConEtiquetaFinalRequerida.add("sub");
        etiquetasConEtiquetaFinalRequerida.add("sup");
        etiquetasConEtiquetaFinalRequerida.add("table");
        etiquetasConEtiquetaFinalRequerida.add("textarea");
        etiquetasConEtiquetaFinalRequerida.add("time");
        etiquetasConEtiquetaFinalRequerida.add("title");
        etiquetasConEtiquetaFinalRequerida.add("ul");
        etiquetasConEtiquetaFinalRequerida.add("var");
        etiquetasConEtiquetaFinalRequerida.add("video");
        etiquetasConEtiquetaFinalOpcional = new HashSet<>();
        etiquetasConEtiquetaFinalOpcional.add("body");
        etiquetasConEtiquetaFinalOpcional.add("colgroup");
        etiquetasConEtiquetaFinalOpcional.add("dd");
        etiquetasConEtiquetaFinalOpcional.add("dt");
        etiquetasConEtiquetaFinalOpcional.add("head");
        etiquetasConEtiquetaFinalOpcional.add("html");
        etiquetasConEtiquetaFinalOpcional.add("li");
        etiquetasConEtiquetaFinalOpcional.add("optgroup");
        etiquetasConEtiquetaFinalOpcional.add("option");
        etiquetasConEtiquetaFinalOpcional.add("p");
        etiquetasConEtiquetaFinalOpcional.add("tbody");
        etiquetasConEtiquetaFinalOpcional.add("td");
        etiquetasConEtiquetaFinalOpcional.add("tfoot");
        etiquetasConEtiquetaFinalOpcional.add("th");
        etiquetasConEtiquetaFinalOpcional.add("thead");
        etiquetasConEtiquetaFinalOpcional.add("tr");
        etiquetasConEtiquetaFinalVacia = new HashSet<>();
        etiquetasConEtiquetaFinalVacia.add("area");
        etiquetasConEtiquetaFinalVacia.add("base");
        etiquetasConEtiquetaFinalVacia.add("br");
        etiquetasConEtiquetaFinalVacia.add("col");
        etiquetasConEtiquetaFinalVacia.add("command");
        etiquetasConEtiquetaFinalVacia.add("embed");
        etiquetasConEtiquetaFinalVacia.add("hr");
        etiquetasConEtiquetaFinalVacia.add("img");
        etiquetasConEtiquetaFinalVacia.add("input");
        etiquetasConEtiquetaFinalVacia.add("link");
        etiquetasConEtiquetaFinalVacia.add("meta");
        etiquetasConEtiquetaFinalVacia.add("param");
        etiquetasConEtiquetaFinalVacia.add("source");
    }

    private Path filePath;
    private List<String> textoHTML;
    private boolean documentoValido;
    private boolean documentoValidado;
    private final List<ErrorDeValidacion> errores;
    private final AutomataHTML automata;
    private final Stack<String> etiquetas;
    
    public ValidadorHTML(List<String> textoHTML) {
        this.textoHTML = textoHTML;
        automata = new AutomataHTML();
        errores = new ArrayList<>();
        etiquetas = new Stack<>();
    }
    
    public ValidadorHTML(Path filePath) throws IOException {
        this(Files.readAllLines(filePath, Charset.defaultCharset()));
        this.filePath = filePath;
    }

    private class AutomataHTML {

        private int estado = 0;
        private final StringBuilder etiqueta = new StringBuilder();
        
        public void reset() {
            estado = 0;
            etiqueta.delete(0, etiqueta.length());
        }
        
        public Object consumirCaracter(char caracter, boolean ultimoCaracter) {
            Object toReturn = null;

            switch (estado) {
                case 0:
                    if (caracter == '<') {
                        estado = 1;
                    }

                    break;
                case 1:
                    if (Character.isLetter(caracter)) {
                        estado = 2;
                        etiqueta.append(caracter);
                    } else if (caracter == '/') {
                        estado = 3;
                    } else {
                        if (caracter != '<') {
                            estado = 0;
                        }
                    }
                    break;
                case 2:
                    if (caracter == '>') {
                        estado = 4;
                    } else if (Character.isWhitespace(caracter)) {
                        estado = 5;
                    } else if (caracter == '/') {
                        estado = 6;
                    } else {
                        if (Character.isLetterOrDigit(caracter)) {
                            etiqueta.append(caracter);
                        } else {
                            estado = 0;
                            toReturn = TipoErrorDeValidacion.ETIQUETA_INVALIDA;
                            etiqueta.delete(0, etiqueta.length());
                        }
                    }
                    break;
                case 3:
                    if (Character.isLetter(caracter)) {
                        estado = 9;
                        etiqueta.append(caracter);
                    } else {
                        estado = 0;
                        toReturn = TipoErrorDeValidacion.ETIQUETA_DE_CIERRE_INVALIDA;
                    }
                    break;
                case 4:
                    estado = caracter == '<' ? 1 : 0;
//                    System.out.print("\nLa etiqueta es: " + etiqueta + "\n");

                    if (etiquetasConEtiquetaFinalRequerida.contains(etiqueta.toString())
                            || etiquetasConEtiquetaFinalOpcional.contains(etiqueta.toString())) {
                        etiquetas.push(etiqueta.toString());
                    } else {
                        if (etiquetasConEtiquetaFinalVacia.contains(etiqueta.toString())) {
                            toReturn = TipoErrorDeValidacion.ETIQUETA_VACIA_NO_CERRADA;
                        } else {
                            toReturn = TipoErrorDeValidacion.ETIQUETA_DESCONOCIDA;
                        }
                    }

                    etiqueta.delete(0, etiqueta.length());
                    break;
                case 5:
                    if (caracter == '>') {
                        estado = 4;
                    } else if (caracter == '/') {
                        estado = 7;
                    }

                    break;
                case 6:
                    if (caracter == '>') {
                        estado = 8;
                    } else {
                        estado = 0;
                        toReturn = TipoErrorDeValidacion.ETIQUETA_DE_CIERRE_INVALIDA;
                        etiqueta.delete(0, etiqueta.length());
                    }

                    break;
                case 7:
                    if (caracter == '>') {
                        estado = 8;
                    } else {
                        estado = 5;
                    }

                    break;
                case 8:
                    estado = caracter == '<' ? 1 : 0;
//                    System.out.print("\nLa etiqueta es: " + etiqueta + "\n");

                    if (etiquetasConEtiquetaFinalRequerida.contains(etiqueta.toString())) {
                        toReturn = TipoErrorDeValidacion.TIPO_ETIQUETA_REQUERIDA_INVALIDA;
                    } else {
                        if (!(etiquetasConEtiquetaFinalOpcional.contains(etiqueta.toString())
                                || etiquetasConEtiquetaFinalVacia.contains(etiqueta.toString()))) {
                            toReturn = TipoErrorDeValidacion.ETIQUETA_DESCONOCIDA;
                        }
                    }

                    etiqueta.delete(0, etiqueta.length());
                    break;
                case 9:
                    if (Character.isWhitespace(caracter)) {
                        estado = 11;
                    } else if (caracter == '>') {
                        estado = 10;
                    } else {
                        if (Character.isLetterOrDigit(caracter)) {
                            etiqueta.append(caracter);
                        } else {
                            estado = 0;
                            toReturn = TipoErrorDeValidacion.ETIQUETA_DE_CIERRE_INVALIDA;
                            etiqueta.delete(0, etiqueta.length());
                        }
                    }
                    break;
                case 10:
                    estado = caracter == '<' ? 1 : 0;
//                    System.out.print("\nLa etiqueta es: " + etiqueta + "\n");

                    if (etiquetas.peek().equals(etiqueta.toString())) {
                        etiquetas.pop();
                    } else {
                        if (etiquetasConEtiquetaFinalRequerida.contains(etiqueta.toString())) {
                            toReturn = TipoErrorDeValidacion.ETIQUETA_DE_INICIO_FALTANTE;
                        } else if (etiquetasConEtiquetaFinalVacia.contains(etiqueta.toString())) {
                            toReturn = TipoErrorDeValidacion.ETIQUETA_VACIA_CERRADA_INCORRECTAMENTE;
                        } else if (etiquetasConEtiquetaFinalOpcional.contains(etiqueta.toString())) {
                            toReturn = TipoErrorDeValidacion.ETIQUETA_OPCIONAL_CERRADA_INCORRECTAMENTE;
                        } else {
                            toReturn = TipoErrorDeValidacion.ETIQUETA_DE_INICIO_FALTANTE;
                        }
                    }

                    etiqueta.delete(0, etiqueta.length());
                    break;
                case 11:
                    if (caracter == '>') {
                        estado = 10;
                    } else {
                        if (!Character.isWhitespace(caracter)) {
                            estado = 0;
                            toReturn = TipoErrorDeValidacion.ETIQUETA_DE_CIERRE_INVALIDA;
                            etiqueta.delete(0, etiqueta.length());
                        }
                    }
                    break;
                default:
                    break;
            }

            if (ultimoCaracter) {
                switch (estado) {
                    case 4:
//                        System.out.print("\nLa etiqueta es: " + etiqueta + "\n");
                        if (etiquetasConEtiquetaFinalRequerida.contains(etiqueta.toString())
                                || etiquetasConEtiquetaFinalOpcional.contains(etiqueta.toString())) {
                            etiquetas.push(etiqueta.toString());
                        } else {
                            if (etiquetasConEtiquetaFinalVacia.contains(etiqueta.toString())) {
                                toReturn = TipoErrorDeValidacion.ETIQUETA_VACIA_NO_CERRADA;
                            } else {
                                toReturn = TipoErrorDeValidacion.ETIQUETA_DESCONOCIDA;
                            }
                        }
                    case 8:
//                        System.out.print("\nLa etiqueta es: " + etiqueta + "\n");
                        if (etiquetasConEtiquetaFinalRequerida.contains(etiqueta.toString())) {
                            toReturn = TipoErrorDeValidacion.TIPO_ETIQUETA_REQUERIDA_INVALIDA;
                        } else {
                            if (!(etiquetasConEtiquetaFinalOpcional.contains(etiqueta.toString())
                                    || etiquetasConEtiquetaFinalVacia.contains(etiqueta.toString()))) {
                                toReturn = TipoErrorDeValidacion.ETIQUETA_DESCONOCIDA;
                            }
                        }
                    case 10:
//                        System.out.print("\nLa etiqueta es: " + etiqueta + "\n");
                        if (etiquetas.peek().equals(etiqueta.toString())) {
                            etiquetas.pop();
                        } else {
                            if (etiquetasConEtiquetaFinalRequerida.contains(etiquetas.peek())) {
                                toReturn = TipoErrorDeValidacion.ETIQUETA_DE_INICIO_FALTANTE;
                            } else if (etiquetasConEtiquetaFinalVacia.contains(etiquetas.peek())) {
                                toReturn = TipoErrorDeValidacion.ETIQUETA_VACIA_CERRADA_INCORRECTAMENTE;
                            } else if (etiquetasConEtiquetaFinalOpcional.contains(etiquetas.peek())) {
                                toReturn = TipoErrorDeValidacion.ETIQUETA_OPCIONAL_CERRADA_INCORRECTAMENTE;
                            } else {
                                toReturn = TipoErrorDeValidacion.ETIQUETA_DE_INICIO_FALTANTE;
                            }
                        }

                        etiqueta.delete(0, etiqueta.length());
                        break;
                    case 2:
                    case 3:
                    case 5:
                    case 6:
                    case 7:
                    case 9:
                    case 11:
                        toReturn = TipoErrorDeValidacion.ETIQUETA_INCOMPLETA;
                        break;
                    default:
//                        System.out.println("Terminó con estado: " + estado);
                        break;
                }

                //reset estado
                estado = 0;
            }

            return toReturn;
        }
    }

    public void validarDocumento() {
        automata.reset();
        errores.clear();
        Object salida;        
        documentoValido = false;
        
        int fila;
        int columna = 0;
        for (fila = 0; fila < textoHTML.size(); fila++) {
            System.out.print(fila + 1);
            System.out.print(" ");
            String linea = textoHTML.get(fila);

            for (columna = 0; columna < linea.length(); columna++) {
                char caracter = linea.charAt(columna);
                salida = automata.consumirCaracter(caracter, fila == textoHTML.size() - 1 && columna == linea.length() - 1);

                if (salida != null) {
                    if (salida instanceof TipoErrorDeValidacion) {
                        errores.add(new ErrorDeValidacion((TipoErrorDeValidacion) salida, fila + 1, (columna + 1 < linea.length() ? columna + 1 : columna + 2)));
                        if (documentoValido) {
                            documentoValido = false;
                        }
                    }
                }

                System.out.print(caracter);
            }
            
            System.out.println();
        }
        
        for (int i = 0; i < etiquetas.size(); i++) {
            String etiqueta = etiquetas.pop();
            
            if (etiquetasConEtiquetaFinalRequerida.contains(etiqueta)) {
                errores.add(new ErrorDeValidacion(TipoErrorDeValidacion.ETIQUETA_REQUERIDA_NO_CERRADA, fila + 1, columna + 1));
            } else {
                errores.add(new ErrorDeValidacion(TipoErrorDeValidacion.ETIQUETA_OPCIONAL_NO_CERRADA, fila + 1, columna + 1));
            }
        }
        
        documentoValidado = true;
        documentoValido = errores.isEmpty();
    }

    public List<ErrorDeValidacion> obtenerErrores() {
        return errores;
    }

    public boolean esDocumentoValido() {
        return documentoValido;
    }

    public boolean estaValidado() {
        return documentoValidado;
    }

    public void setTextoHTML(List<String> textoHTML) {
        this.textoHTML = textoHTML;
    }

    public List<String> getTextoHTML() {
        return textoHTML;
    }
    
    public Path getPath() {
        return filePath;
    }
    
    public void setPath(Path filePath) throws IOException {
        this.setTextoHTML(Files.readAllLines(filePath, Charset.defaultCharset()));
        this.filePath = filePath;
    }
}